#include <stdint.h>
#include <signal.h>
#include "mem.h"
#include "async.h"
#include "redobject.h"
#include "redintobject.h"
#include "redmapobject.h"
#include "rednullobject.h"
#include "redtupleobject.h"
#include "redstringobject.h"
#include "redexceptionobject.h"

static const char *SIGNAMES[] =
{
	"SIGHUP",
	"SIGINT",
	"SIGQUIT",
	"SIGILL",
	"SIGTRAP",
	"SIGABRT",
	"SIGEMT",
	"SIGFPE",
	"SIGKILL",
	"SIGBUS",
	"SIGSEGV",
	"SIGSYS",
	"SIGPIPE",
	"SIGALRM",
	"SIGTERM",
	"SIGURG",
	"SIGSTOP",
	"SIGTSTP",
	"SIGCONT",
	"SIGCHLD",
	"SIGTTIN",
	"SIGTTOU",
	"SIGIO",
	"SIGXCPU",
	"SIGXFSZ",
	"SIGVTALRM",
	"SIGPROF",
	"SIGWINCH",
	"SIGINFO",
	"SIGUSR1",
	"SIGUSR2",
};

typedef struct _sig_ctx_t
{
	int			 signal;
	RedObject	*callable;
} sig_ctx_t;

static RedObject *signals[NSIG] = {NULL};

static void run_handler(void *args)
{
	if (args)
	{
		sig_ctx_t *ctx = (sig_ctx_t *)args;
		RedObject *signal = RedInt_FromInt(ctx->signal);

		if (signal)
		{
			RedObject *tuple = RedTuple_FromObjects(1, signal);

			if (tuple)
			{
				RedObject *result = RedObject_Invoke(ctx->callable, tuple);

				Red_UNREF(tuple);
				Red_UNREF(result);
			}

			Red_UNREF(signal);
		}

		RedMem_Free(ctx);
	}
}

static void signal_handler(int sig)
{
	if (signals[sig])
	{
		sig_ctx_t *ctx = RedMem_Alloc(sizeof(sig_ctx_t));

		if (!ctx)
		{
			/* just force the eval-loop to check for exceptions */
			RedAsync_Schedule(run_handler, NULL);
			return;
		}

		ctx->signal = sig;
		ctx->callable = signals[sig];
		RedAsync_Schedule(run_handler, ctx);
	}
}

static RedObject *signal_signal(RedObject *signum, RedObject *callable)
{
	if (!RedInt_Exact(signum))
	{
		RedException_ThrowFormat(&RedExc_TypeError, "Signal numbers must be integers");
		return NULL;
	}

	int64_t sig = RedInt_Value(signum);

	if (sig < SIGHUP || sig > SIGUSR2)
	{
		RedException_ThrowFormat(&RedExc_ValueError, "Invalid signal number");
		return NULL;
	}

	if ((callable == Red_Null) ||
		(RedInt_Exact(callable) && RedInt_Value(callable) == (intptr_t)SIG_DFL))
	{
		RedObject *prev = signals[sig];

		if (!siginterrupt((int)sig, 0) && signal((int)sig, SIG_DFL) != SIG_ERR)
		{
			Red_UNREF(signals[sig]);
			return prev ? prev : (Red_REF(Red_Null), Red_Null);
		}

		RedException_ThrowErrno(&RedExc_OSError);
		return NULL;
	}
	else if ((RedInt_Exact(callable) && RedInt_Value(callable) == (intptr_t)SIG_IGN))
	{
		RedObject *prev = signals[sig];

		if (!siginterrupt((int)sig, 0) && signal((int)sig, SIG_IGN) != SIG_ERR)
		{
			Red_UNREF(signals[sig]);
			return prev ? prev : (Red_REF(Red_Null), Red_Null);
		}

		RedException_ThrowErrno(&RedExc_OSError);
		return NULL;
	}
	else if (RedObject_Callable(callable))
	{
		if (siginterrupt((int)sig, 1) || signal((int)sig, signal_handler) == SIG_ERR)
		{
			RedException_ThrowErrno(&RedExc_OSError);
			return NULL;
		}

		RedObject *prev = signals[sig];

		Red_REF(callable);
		signals[sig] = callable;
		return prev ? prev : (Red_REF(Red_Null), Red_Null);
	}
	else
	{
		RedException_ThrowFormat(&RedExc_ValueError, "Invalid signal handler, maybe one of null/callables/SIG_DFL/SIG_IGN");
		return NULL;
	}
}

static void module_exit(void *args)
{
	for (int s = SIGHUP; s <= SIGUSR2; s++)
		Red_UNREF(signals[s]);
}

RED_EXPORT char module_entry(RedObject *self, RedObject *name)
{
	RedObject *names = RedMap_Create();

	for (int s = SIGHUP; s <= SIGUSR2; s++)
	{
		RedObject *sig = RedInt_FromInt(s);
		RedObject *name = RedString_FromCString(SIGNAMES[s - 1]);

		if (!RedMap_PutString(names, sig, name) ||
			!RedMap_PutString(self->ob_attrs, name, sig))
		{
			Red_UNREF(sig);
			Red_UNREF(name);
			Red_UNREF(names);
			return 0;
		}

		Red_UNREF(sig);
		Red_UNREF(name);
	}

	if (!RedObject_AddBinary(self, "signal", signal_signal)) return 0;

	if (!RedObject_AddObject(self, "SIGNALS"	, names))								return 0;
	if (!RedObject_AddObject(self, "SIG_DFL"	, RedInt_FromInt((intptr_t)SIG_DFL)))	return 0;
	if (!RedObject_AddObject(self, "SIG_ERR"	, RedInt_FromInt((intptr_t)SIG_ERR)))	return 0;
	if (!RedObject_AddObject(self, "SIG_IGN"	, RedInt_FromInt((intptr_t)SIG_IGN)))	return 0;

	Red_ExitFunction(module_exit, NULL);
	return 1;
}
